/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.spring.beans.factory.annotation;

import com.alibaba.spring.util.PropertySourcesUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import static com.alibaba.spring.beans.factory.annotation.ConfigurationBeanBindingPostProcessor.initBeanMetadataAttributes;
import static com.alibaba.spring.util.BeanRegistrar.registerInfrastructureBean;
import static com.alibaba.spring.util.PropertySourcesUtils.getSubProperties;
import static com.alibaba.spring.util.PropertySourcesUtils.normalizePrefix;
import static java.util.Collections.singleton;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition;
import static org.springframework.core.annotation.AnnotationAttributes.fromMap;

/**
 * The {@link ImportBeanDefinitionRegistrar} implementation for {@link EnableConfigurationBeanBinding @EnableConfigurationBinding}
 *
 * @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 * @since 1.0.3
 */
public class ConfigurationBeanBindingRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    final static Class ENABLE_CONFIGURATION_BINDING_CLASS = EnableConfigurationBeanBinding.class;

    private final static String ENABLE_CONFIGURATION_BINDING_CLASS_NAME = ENABLE_CONFIGURATION_BINDING_CLASS.getName();

    private final Log log = LogFactory.getLog(getClass());

    private ConfigurableEnvironment environment;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {

        AnnotationAttributes attributes = fromMap(metadata.getAnnotationAttributes(ENABLE_CONFIGURATION_BINDING_CLASS_NAME));

        registerConfigurationBeanDefinitions(attributes, registry);
    }

    /**
     * 这里就是根据每个@EnableConfigurationBeanBinding注解信息 ，获取对应的配置创建相应的配置类对象
     * 比如dubbo.application对应到了ApplicationConfig这个对象，其他的类似
     * @param attributes
     * @param registry
     */
    protected void registerConfigurationBeanDefinitions(AnnotationAttributes attributes, BeanDefinitionRegistry registry) {

        String prefix = environment.resolvePlaceholders(attributes.getString("prefix"));

        //从注解中获取type，对应的比如ApplicationConfig的class 类型
        Class<?> configClass = attributes.getClass("type");

        //当前处理的是单数的还是复数的
        boolean multiple = attributes.getBoolean("multiple");

        //获取是否忽略未知的字段属性，在注解EnableConfigurationBeanBinding中默认是true
        boolean ignoreUnknownFields = attributes.getBoolean("ignoreUnknownFields");

        boolean ignoreInvalidFields = attributes.getBoolean("ignoreInvalidFields");

        //根据配置对应的配置类比如ApplicationConfig，生成一个BeanDefinition放入spring容器
        registerConfigurationBeans(prefix, configClass, multiple, ignoreUnknownFields, ignoreInvalidFields, registry);
    }

    private void registerConfigurationBeans(String prefix, Class<?> configClass, boolean multiple,
                                            boolean ignoreUnknownFields, boolean ignoreInvalidFields,
                                            BeanDefinitionRegistry registry) {
        //这个处理就是根据配置的前缀从environment中获取对应的参数值，比如dubbo.application.name=proivder-service
        /**
         * 那么configurationProperties返回的就是
         * name=proivder-service
         * 比如是
         * dubbo.protocols.p1.id=dubbo-p1
         * dubbo.protocols.p1.name=dubbo
         * dubbo.protocols.p1.port=30881
         * dubbo.protocols.p1.host=0.0.0.0
         * 那么返回的就是：
         *  p1.id=dubbo-p1
         *  p1.name=dubbo
         *  p1.port=30881
         *  p1.host=0.0.0.0
         */
        Map<String, Object> configurationProperties = PropertySourcesUtils.getSubProperties(environment.getPropertySources(), environment, prefix);

        //如果根据这个前缀没有获取到指定的配置信息，那么证明没有配置，直接返回，不用往下处理
        if (CollectionUtils.isEmpty(configurationProperties)) {
            if (log.isDebugEnabled()) {
                log.debug("There is no property for binding to configuration class [" + configClass.getName()
                        + "] within prefix [" + prefix + "]");
            }
            return;
        }

        /**
         * 这里就是根据获取到的配置去生成bean的名字，如果是dubbo.protocols.p1这种，那么生成的名字
         * 就是p1，p2这种，配置了几个就生成几个name，这些name要作为一个bean生成BeanDefinition放入spring容器中
         * 如果说是单个的配置，那么生成的名字就是调用spring的名字生成器生成的
         * 如果是单数，那么beanNames就只有一个，如果是复数就可能有多个
         * 单个的比如：org.apache.dubbo.config.ApplicationConfig#0
         */
        Set<String> beanNames = multiple ? resolveMultipleBeanNames(configurationProperties) :
                singleton(resolveSingleBeanName(configurationProperties, configClass, registry));

        //根据bean的名字集合创建BeanDefinition放入spring容器
        for (String beanName : beanNames) {
            registerConfigurationBean(beanName, configClass, multiple, ignoreUnknownFields, ignoreInvalidFields,
                    configurationProperties, registry);
        }

        //上面是将配置类作为一个BeanDefinition注册到spring容器中，但是配置信息还没有赋值，还都是空的
        //所以下面的这个方法就是又向spring容器注册了一个bean的后置处理器，进行赋值
        //注册的后置处理器是ConfigurationBeanBindingPostProcessor，这个后置处理器
        //就是向每个bean的属性进行赋值，比如配置的dubbo.application.name，那么对应的是
        //ApplicationConfig这个bean，后置处理器就是为ApplicationConfig这个bean的对象赋值
        //比如复制name属性，那么思考一下为什么要通过后置处理器来赋值呢？因为这里还只是向spring容器中
        //注册了BeanDefinition，此时还没有对象产生，需要在bean的实例化后才能赋值，所以在bean的后置处理器
        //中，调用bean的初始化调用方法进行赋值，因为配置信息上面已经设置到了对应的BeanDefinition中了
        //所以在后置处理器中拿到这个配置信息，赋值给对应的bean

        registerConfigurationBindingBeanPostProcessor(registry);
    }

    private void registerConfigurationBean(String beanName, Class<?> configClass, boolean multiple,
                                           boolean ignoreUnknownFields, boolean ignoreInvalidFields,
                                           Map<String, Object> configurationProperties,
                                           BeanDefinitionRegistry registry) {

        //创建一个BeanDefinition的构建器，是通过配置类构建的
        BeanDefinitionBuilder builder = rootBeanDefinition(configClass);

        //通过构建器创建BeanDefinition
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();

        setSource(beanDefinition);

        //加工配置信息
        Map<String, Object> subProperties = resolveSubProperties(multiple, beanName, configurationProperties);

        //最后将配置信息和一些是否忽略的参数设置到beanDefinition
        initBeanMetadataAttributes(beanDefinition, subProperties, ignoreUnknownFields, ignoreInvalidFields);
        //向spring容器注册这个BeanDefinition
        registry.registerBeanDefinition(beanName, beanDefinition);

        if (log.isInfoEnabled()) {
            log.info("The configuration bean definition [name : " + beanName + ", content : " + beanDefinition
                    + "] has been registered.");
        }
    }

    private Map<String, Object> resolveSubProperties(boolean multiple, String beanName,
                                                     Map<String, Object> configurationProperties) {
        if (!multiple) {
            return configurationProperties;
        }

        MutablePropertySources propertySources = new MutablePropertySources();

        propertySources.addLast(new MapPropertySource("_", configurationProperties));

        return getSubProperties(propertySources, environment, normalizePrefix(beanName));
    }

    private void setSource(AbstractBeanDefinition beanDefinition) {
        beanDefinition.setSource(ENABLE_CONFIGURATION_BINDING_CLASS);
    }

    private void registerConfigurationBindingBeanPostProcessor(BeanDefinitionRegistry registry) {
        registerInfrastructureBean(registry, ConfigurationBeanBindingPostProcessor.BEAN_NAME,
                ConfigurationBeanBindingPostProcessor.class);
    }

    @Override
    public void setEnvironment(Environment environment) {

        Assert.isInstanceOf(ConfigurableEnvironment.class, environment);

        this.environment = (ConfigurableEnvironment) environment;

    }

    private Set<String> resolveMultipleBeanNames(Map<String, Object> properties) {

        Set<String> beanNames = new LinkedHashSet<String>();

        for (String propertyName : properties.keySet()) {

            int index = propertyName.indexOf(".");

            if (index > 0) {

                String beanName = propertyName.substring(0, index);

                beanNames.add(beanName);
            }

        }

        return beanNames;

    }

    private String resolveSingleBeanName(Map<String, Object> properties, Class<?> configClass,
                                         BeanDefinitionRegistry registry) {

        String beanName = (String) properties.get("id");

        if (!StringUtils.hasText(beanName)) {
            BeanDefinitionBuilder builder = rootBeanDefinition(configClass);
            beanName = BeanDefinitionReaderUtils.generateBeanName(builder.getRawBeanDefinition(), registry);
        }

        return beanName;

    }
}
